/* * Copyright 2015-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.dataflow.rest.client; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.StepExecution; import org.springframework.batch.item.ExecutionContext; import org.springframework.cloud.dataflow.rest.Version; import org.springframework.cloud.dataflow.rest.client.support.ExecutionContextJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.ExitStatusJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.JobExecutionJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.JobInstanceJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.JobParameterJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.JobParametersJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.StepExecutionHistoryJacksonMixIn; import org.springframework.cloud.dataflow.rest.client.support.StepExecutionJacksonMixIn; import org.springframework.cloud.dataflow.rest.job.StepExecutionHistory; import org.springframework.cloud.dataflow.rest.resource.RootResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; /** * Implementation of DataFlowOperations delegating to sub-templates, discovered via REST * relations. * * @author Ilayaperumal Gopinathan * @author Mark Fisher * @author Glenn Renfro * @author Patrick Peralta * @author Gary Russell * @author Eric Bottard * @author Gunnar Hillert */ public class DataFlowTemplate implements DataFlowOperations { /** * A template used for http interaction. */ protected final RestTemplate restTemplate; /** * Holds discovered URLs of the API. */ protected final Map<String, UriTemplate> resources = new HashMap<String, UriTemplate>(); /** * REST client for stream operations. */ private final StreamOperations streamOperations; /** * REST client for counter operations. */ private final CounterOperations counterOperations; /** * REST client for field value counter operations. */ private final FieldValueCounterOperations fieldValueCounterOperations; /** * REST client for aggregate counter operations. */ private final AggregateCounterOperations aggregateCounterOperations; /** * REST client for task operations. */ private final TaskOperations taskOperations; /** * REST client for job operations. */ private final JobOperations jobOperations; /** * REST client for app registry operations. */ private final AppRegistryOperations appRegistryOperations; /** * REST client for completion operations. */ private final CompletionOperations completionOperations; /** * REST Client for runtime operations. */ private final RuntimeOperations runtimeOperations; /** * REST Client for "about" operations. */ private final AboutOperations aboutOperations; /** * Setup a {@link DataFlowTemplate} using the provided baseURI. Will create a * {@link RestTemplate} implicitly with the required set of Jackson MixIns. For more * information, please see {@link #prepareRestTemplate(RestTemplate)}. * <p> * Please be aware that the created RestTemplate will use the JDK's default timeout * values. Consider passing in a custom {@link RestTemplate} or, depending on your JDK * implementation, set System properties such as: * <p> * <ul> * <li>sun.net.client.defaultConnectTimeout * <li>sun.net.client.defaultReadTimeout * </ul> * <p> * For more information see <a href= * "http://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html">this * link</a> * * @param baseURI Must not be null */ public DataFlowTemplate(URI baseURI) { this(baseURI, getDefaultDataflowRestTemplate()); } /** * Setup a {@link DataFlowTemplate} using the provide {@link RestTemplate}. Any * missing Mixins for Jackson will be added implicitly. For more information, please * see {@link #prepareRestTemplate(RestTemplate)}. * * @param baseURI Must not be null * @param restTemplate Must not be null */ public DataFlowTemplate(URI baseURI, RestTemplate restTemplate) { Assert.notNull(baseURI, "The provided baseURI must not be null."); Assert.notNull(restTemplate, "The provided restTemplate must not be null."); this.restTemplate = prepareRestTemplate(restTemplate); final RootResource resourceSupport = restTemplate.getForObject(baseURI, RootResource.class); if (resourceSupport != null) { if (resourceSupport.getApiRevision() == null) { throw new IllegalStateException("Incompatible version of Data Flow server detected.\n" + "Follow instructions in the documentation for the version of the server you are " + "using to download a compatible version of the shell.\n" + "Documentation can be accessed at http://cloud.spring.io/spring-cloud-dataflow/"); } String serverRevision = resourceSupport.getApiRevision().toString(); if (!String.valueOf(Version.REVISION).equals(serverRevision)) { String downloadURL = getLink(resourceSupport, "dashboard").getHref() + "#about"; throw new IllegalStateException(String.format( "Incompatible version of Data Flow server detected.\n" + "Trying to use shell which supports revision %s, while server revision is %s. Both " + "revisions should be aligned.\n" + "Follow instructions at %s to download a compatible version of the shell.", Version.REVISION, serverRevision, downloadURL)); } this.aboutOperations = new AboutTemplate(restTemplate, resourceSupport.getLink(AboutTemplate.ABOUT_REL)); if (resourceSupport.hasLink(StreamTemplate.DEFINITIONS_REL)) { this.streamOperations = new StreamTemplate(restTemplate, resourceSupport); this.runtimeOperations = new RuntimeTemplate(restTemplate, resourceSupport); } else { this.streamOperations = null; this.runtimeOperations = null; } if (resourceSupport.hasLink(CounterTemplate.COUNTER_RELATION)) { this.counterOperations = new CounterTemplate(restTemplate, resourceSupport); this.fieldValueCounterOperations = new FieldValueCounterTemplate(restTemplate, resourceSupport); this.aggregateCounterOperations = new AggregateCounterTemplate(restTemplate, resourceSupport); } else { this.counterOperations = null; this.fieldValueCounterOperations = null; this.aggregateCounterOperations = null; } if (resourceSupport.hasLink(TaskTemplate.DEFINITIONS_RELATION)) { this.taskOperations = new TaskTemplate(restTemplate, resourceSupport); this.jobOperations = new JobTemplate(restTemplate, resourceSupport); } else { this.taskOperations = null; this.jobOperations = null; } this.appRegistryOperations = new AppRegistryTemplate(restTemplate, resourceSupport); this.completionOperations = new CompletionTemplate(restTemplate, resourceSupport.getLink("completions/stream"), resourceSupport.getLink("completions/task")); } else { this.aboutOperations = null; this.streamOperations = null; this.runtimeOperations = null; this.counterOperations = null; this.fieldValueCounterOperations = null; this.aggregateCounterOperations = null; this.taskOperations = null; this.jobOperations = null; this.appRegistryOperations = null; this.completionOperations = null; } } /** * Will augment the provided {@link RestTemplate} with the Jackson Mixins required by * Spring Cloud Data Flow, specifically: * <p> * <ul> * <li>{@link JobExecutionJacksonMixIn} * <li>{@link JobParametersJacksonMixIn} * <li>{@link JobParameterJacksonMixIn} * <li>{@link JobInstanceJacksonMixIn} * <li>{@link ExitStatusJacksonMixIn} * <li>{@link StepExecutionJacksonMixIn} * <li>{@link ExecutionContextJacksonMixIn} * <li>{@link StepExecutionHistoryJacksonMixIn} * </ul> * <p> * Furthermore, this method will also register the {@link Jackson2HalModule} * * @param restTemplate Can be null. Instantiates a new {@link RestTemplate} if null * @return RestTemplate with the required Jackson Mixins */ public static RestTemplate prepareRestTemplate(RestTemplate restTemplate) { if (restTemplate == null) { restTemplate = new RestTemplate(); } restTemplate.setErrorHandler(new VndErrorResponseErrorHandler(restTemplate.getMessageConverters())); boolean containsMappingJackson2HttpMessageConverter = false; for (HttpMessageConverter<?> converter : restTemplate.getMessageConverters()) { if (converter instanceof MappingJackson2HttpMessageConverter) { containsMappingJackson2HttpMessageConverter = true; final MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter; jacksonConverter.getObjectMapper().registerModule(new Jackson2HalModule()) .addMixIn(JobExecution.class, JobExecutionJacksonMixIn.class) .addMixIn(JobParameters.class, JobParametersJacksonMixIn.class) .addMixIn(JobParameter.class, JobParameterJacksonMixIn.class) .addMixIn(JobInstance.class, JobInstanceJacksonMixIn.class) .addMixIn(ExitStatus.class, ExitStatusJacksonMixIn.class) .addMixIn(StepExecution.class, StepExecutionJacksonMixIn.class) .addMixIn(ExecutionContext.class, ExecutionContextJacksonMixIn.class) .addMixIn(StepExecutionHistory.class, StepExecutionHistoryJacksonMixIn.class); } } if (!containsMappingJackson2HttpMessageConverter) { throw new IllegalArgumentException( "The RestTemplate does not contain a required " + "MappingJackson2HttpMessageConverter."); } return restTemplate; } /** * Invokes {@link #prepareRestTemplate(RestTemplate)}. * * @return RestTemplate with the required Jackson MixIns applied */ public static RestTemplate getDefaultDataflowRestTemplate() { return prepareRestTemplate(null); } public Link getLink(ResourceSupport resourceSupport, String rel) { Link link = resourceSupport.getLink(rel); if (link == null) { throw new DataFlowServerException( "Server did not return a link for '" + rel + "', links: '" + resourceSupport + "'"); } return link; } @Override public StreamOperations streamOperations() { return streamOperations; } @Override public CounterOperations counterOperations() { return counterOperations; } @Override public FieldValueCounterOperations fieldValueCounterOperations() { return fieldValueCounterOperations; } @Override public AggregateCounterOperations aggregateCounterOperations() { return aggregateCounterOperations; } @Override public TaskOperations taskOperations() { return taskOperations; } @Override public JobOperations jobOperations() { return jobOperations; } @Override public AppRegistryOperations appRegistryOperations() { return appRegistryOperations; } @Override public CompletionOperations completionOperations() { return completionOperations; } @Override public RuntimeOperations runtimeOperations() { return runtimeOperations; } @Override public AboutOperations aboutOperation() { return aboutOperations; } /** * @return The underlying RestTemplate, will never return null */ public RestTemplate getRestTemplate() { return restTemplate; } }